Глава 13. RTTI и приведение типов

Аббревиатура RTTI означает RunTime Type Identification, т. е. “Идентификация типа времени выполнения”. Это механизм, позволяющий определить тип объекта во время выполнения программы, что очень полезно в иерархии типов, где указатель или ссылка базового класса может ссылаться на представитель любого производного класса. Полиморфные механизмы, конечно, хороши, но выглядят снаружи подобно “черному ящику”. Вы вызываете виртуальные методы, но не знаете, к чему, собственно, они применяются. Иногда требуется точно знать тип объекта. Если же можно с уверенностью идентифицировать типы, открывается возможность безопасного их приведения.

В этой главе рассматриваются RTTI и усовершенствованные операции приведения типа C++.

RTTI

В этом разделе описан синтаксис и даются примеры использования RTTI.

Операция typeid

Для получения информации о типе во время выполнения программы применяется операция typeid:

typeid(имя_ типа) typeid(выражение)

Ее операндом является либо имя типа, либо выражение, оцениваемое как некоторый тип. Операция возвращает константную ссылку на объект класса type_info, объявленный в заголовке typeinfo.h.

Если операция не может определить тип своего операнда, она выбрасывает исключение типа bad_typeid.

Следует помнить, что RTTI в собственном смысле, как динамическое распознавание типа, работает только с полиморфными типами, т. е. классами, имеющими хотя бы одну виртуальную функцию. Если применить операцию typeid к обычному типу, идентификация типа будет произведена статически, т. е. при компиляции.

type info

Класс type_info объявлен следующим образом:

class _TIDIST _rtti type_info {

public:

tpid * tpp;

private:

cdecl type_info(const type info FAR &);

type info & cdecl operator=(const type_info _FAR &);

public:

virtual _cdecl ~type_info() ;

bool cdecl operator==(const type info FAR &) const;

bool cdecl operator!=(const type info FAR &) const;

bool _cdecl before(const type_info _FAR &) const;

const char _FAR *_cdecl name() const;

protected:

cdecl type_info(tpid * tpp) { tpp = tpp; } };

Ключевое слово _rtti перед именем класса гарантирует, что информация о типе для него будет генерироваться вне зависимости от состояния флажка Enable RTTI на странице C++ диалога Project Options (ему соответствует ключ компилятора -rt).

Открытые элементы класса представлены операциями сравнения на равенство и неравенство, а также функциями name () и before (). Первая возвращает указатель на символьную строку с именем типа. Вторая возвращает true, если класс ее объекта является базовым по отношению к классу аргумента.

Вот пример с использованием операции typeid и класса type_info:

Листинг 13.1. Операция typeid

///////////////////////////////////

// Typeinfo.срр: Операция typeid.

//

#include <typeinfo.h>

#include <iostream.h>

#include <string.h>

#pragma hdrstop

#include <condefs.h>

class Base { // Базовый класс.

public:

virtual ~Base (){} };

class Derived: public Base { // Производный класс.

char *str;

public:

Derived(const char *s) {

str = new char[strien(s)+1];

strcpy(str, s);

}

~Derived() { delete [] str;}

const char *Get() ( return str;}};

int main() {

Derived d("Derived class' string.");

Base &bRef = d; // Базовая ссылка на производный объект.

cout << "Typeinfo of bRef: " << typeid(bRef).name() << end1;

if (typeid(bRef) == typeid(Derived))

cout << "Contents of bRef: "<< ((Derived 6)bRef).Get() << endl;

else

cout << "Cannot cast safely bRef to Derived." << endl;

return 0;

 

Здесь демонстрируется операция typeid, сравнение типов и функция name () класса type_inf о. Программа выводит:

Typeinfo of bRef: Derived

Contents of bRef: Derived class' string.

Сравнение типов объекта bRef и Derived показывает, что они совпадают, и программа приводит ссылку к производному типу. Если закомментировать виртуальный деструктор класса Base, он станет неполиморфным, и typeid уже не сможет определить тип объекта, на который в действительности ссылается bRef:

Typeinfo of bRef: Base

Cannot cast safely bRef to Derived.

Эта программа является примером того, как не следует поступать. Идентифицировать класс, затем привести ссылку к нужному типу — это попытка поставить RTTI на место виртуального механизма. Гораздо проще сделать функцию Get() виртуальной:

class Base { public:

virtual ~Base () { }

virtual const char *Get() { throw MyExcpt; } };

try {

cout << "Contents of bRef: " << bRef.Get() << endl;

} catch(MyExcept) {

}

При недопустимом типе объекта выбрасывается исключение (возможны и другие решения).

RTTI следует применять только в тех случаях, когда тип объекта не известен во время компиляции и нецелесообразно применение других средств C++ вроде позднего связывания.

bad_typeid

Если typeid не может определить тип объекта, выбрасывается исключение bad_typeid. Это происходит, например, при попытке определить тип, на который ссылается нулевой указатель:

/////////////////////////////////////

// BadType.cpp: Исключение bad_typeid.

//

#include <iostream.h> #include <typeinfo.h>

#pragma hdrstop

#include <condefs.h>

class Base { public:

virtual ~Base() {} };

class Derived: public Base {};

int main() {

try {

Base *bp = NULL;

cout<< "Typeid of bp: " << typeid(*bp).name() << endl;

} catch(bad_typeid) {

cout << "Bad typeid caught!"<< endl;

} return 0;

}

Специальные операции приведения типа

Стандарт ANSI определяет специальный синтаксис операций приведения типа, позволяющий программисту воспользоваться преимуществами RTTI и, кроме того, указать точно, что он хочет получить в результате таких операций. Новых операций приведения четыре: dynamic_cast, static cast, reinterpret cast и const_cast.

Здесь нужно вспомнить, для чего вообще может служить приведение типа. Можно назвать следующие случаи:

Эти три случая соответствуют, говоря, может быть, несколько упрощенно, трем последним из перечисленных в начале раздела операций. Операция же dynamic cast позволяет безопасно приводить типы в различных полиморфных иерархиях классов, в том числе с виртуальными базовыми классами.

Мы начнем с более простых и традиционных приведений.

reinterpret_cast

Синтаксис данной формы операции приведения таков:

reinterpret_cast<Целевой_тиn> (аргумент)

Такую операцию можно применить для того, чтобы изменить интерпретацию объекта без действительного преобразования данных.

Целевой_тип может быть типом ссылки, указателя, целым, перечислимым или вещественным типом.

Если целевой_тип — тип указателя или ссылки, то аргумент может быть указателем или ссылкой, а также числовой (вещественной, целой, перечислимой) переменной; когда целевым типом является числовой тип, то операнд может быть указателем или ссылкой.

Операция возвращает значение целевого типа.

Возможно, например, явное преобразование указателя в целый тип, равно как и обратная операция. Можно приводить указатель на функцию одного •типа к указателю на функцию другого типа или на некоторый объект, при условии, что он (указатель на объект) имеет достаточную разрядность.

Вот пример преобразования указателя в целое и наоборот:

//////////////////////////////////

// Reinterpret.срр: Демонстрация reinterpret_cast

//

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

int main () {

int i = 7;

int *ip = Si;

int temp = reinterpret cast<int>(ip);

cout.setf(ios::showbase) ;

cout << "Pointer value is"<< ip << end1;

cout << "Representation of a pointer as int is " << hex << temp << endl;

cout << "Convert it back and dereference:"<<*reinterpret_cast<int*>(temp) << endl;

return 0;

}

Эта программа выводит:

Pointer value is 0065FEOO

Representation of a pointer as int is Ox65fe00

Convert it back and dereference: 0х7

Все это можно проделать, разумеется, и с помощью обычных операций приведения, однако последние мало надежны. Тут при опечатках могут происходить совершенно дикие преобразования, и компилятор не выдает даже предупреждающих сообщений. Специальные же операции имеют более корректный вид и явно показывают, что вы делаете.

const_cast

Операция cons't_cast имеет ту же форму, что и предыдущая:

соnst_сonst<целевой_тип>(аргумент)

Целевой тип, возвращаемый такой операцией, может быть любым и должен отличаться от типа аргумента только модификаторами const и volatile.

Вот пример инициализации динамической константной строки:

///////////////////////////////////////

// ConstCast.срр: Подавление модификатора const.

//

#include <string.h>

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

int main ()

cons с char *ip;

ip = new char[20];

strcpy(const_cast<char*>(ip), "New const string,");

cout << ip << end1; delete [] ip;

return 0;

static_cast

Операция статического приведения типа

static саst<целевой тип> (аргумент)

может выполнять преобразования между числовыми типами, а также между указателями либо ссылками на объекты классов, находящихся в иерархическом отношении (если оно однозначно и базовый класс — не виртуальный). Операция реализуется во время компиляции.

Преобразования числовых типов происходят точно так же, как в случае обычной нотации приведений. Приведение указателей и ссылок возможно как от производного класса к базовому (тут все достаточно просто), так и от базового к производному (нисходящее приведение типа). Конечно, следует помнить, что во многих случаях нисходящее приведение указателя базового типа не будет безопасным, если только он не ссылается в действительности на представитель производного класса.

Если некоторый указатель может быть приведен к типу Т*, то объект этого типа может быть приведен к типу Т&.

Объект или значение могут быть приведены к объекту некоторого класса, если в данном классе объявлен соответствующий конструктор или имеется подходящая операция преобразования. Этот момент продемонстрирован в приведенной ниже программе.

Листинг 13.2. Нисходящее приведение указателей и ссылок

////////////////////////////////////

// StatCast.срр: Статическое нисходящее приведение типа.

//

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

class A {} ;

class B: public A { public:

int i;

B(int ii): i(ii) {}

B(A&): i(11) {

cout << "Derived conversion constructor... ";

} };

int main() {

В b(22), *pb = &b;

A Sra = static cast<A&>(b); // Ссылка на b как

// базовый объект.

А *ра = static_cast<A*>(pb); // Указатель на b как

// базовый объект.

cout << "Derived object: " << b.i << endl;

cout << "Downcasting pointer to pointer: "

<< static_cast<B*>(pa)->i << endl;

// Приведение

// указателей.

cout <<"Downcasting referense to referense: "

<< static cast<B&>(га).i<< endl;

// Приведение

// к ссылке.

cout << "Downcasting reference to object: ";

cout << static cast<B>(ra).i<< endl;

// Приведение

// к объекту.

return 0;

}

Вот что выводит этот код:

Derived object: 22

Downcasting pointer to pointer: 22

Downcasting referense to referense: 22

Downcasting reference to object: Derived conversion

constructor... 11

Как видите, приведение ссылки базового класса к ссылке производного дает ссылку на первоначальный объект производного класса (b), в то время как преобразование той же ссылки в представитель производного класса конструирует новый (временный) объект.

dynamic_cast

Операция динамического приведения типа

dynamic сast<целевой_тип>(аргумент)

не имеет аналогов среди операций, выполняемых .с применением “классической” нотации приведения. Операция и проверка ее корректности при известных условиях происходит во время выполнения программы.

Динамическое приведение типа опирается на механизм RTTI, поэтому необходимо установить флажок Enable RTTI в диалоге Project Options (страница C++). Если этот флажок сброшен, программа компилироваться не будет.

Целевой тип операции должен быть типом указателя, ссылки или void*. Если целевой тип — тип указателя, то аргументом должен быть указатель на объект класса; если целевой тип — ссылка, то аргумент должен также быть соответствующей ссылкой. Если целевым типом является void*, то аргумент также должен быть указателем, а результатом операции будет указатель, с помощью которого можно обратиться к любому элементу “самого производного” класса иерархии, который сам не может быть базовым ни для какого другого класса.

Приведение от производного класса к базовому разрешается на этапе компиляции. Преобразования от базового класса к производному, либо перекрестные преобразования на некоторой иерархии, происходят во время выполнения программы. Операция нисходящего приведения типа допустима только в случае, если базовый класс (класс аргумента) является полиморфным.

При попытке произвести некорректное преобразование операция возвращает нуль, если целевой_тип — указатель. Если ссылка, операция выбрасывает исключение типа bad_cast.

С помощью операции dynamic_cast можно выполнять нисходящее приведение виртуального базового класса, что невозможно сделать с применением обычной нотации приведений, при условии, что базовый класс является полиморфным и преобразование разрешается однозначно.

Ниже показаны две программы, демонстрирующие динамическое приведение типа. В первой из них для контроля успешности преобразований используются исключения, во второй — проверка на равенство результата нулю.

Листинг 13.3. Нисходящее и перекрестное приведение типа

//////////////////////////////////////

// Dynamic.срр: Динамическое приведение типа.

//

#include <iostream.h>

#include <typeinfo.h>

#pragma hdrstop

#include <condefs.h>

class Bl { // Полиморфный базовый класс.

public:

virtual ~B1() {} } ;

class B2 {}; class D:

public Bl,

public B2 {}; // Производный класс.

int main () {

D d;

Bl bl;

Bl &rbl = d;

try { //

// Нисходящее приведение. //

cout <<"Downcasting from Bl; object ID: "

<< typeid(rbl).name() << endl;

D &rd = dynamic_cast<DS>(rbl);

cout << "OK..."<< endl;

//

// Перекрестное приведение.

//

cout << "Cross-castind from Bl to B2; object ID: "

<< typeid(rbl).name() << endl;

B2 &rb2 = dynamic_cast<B2&> (rbl);

cout << "OK..." << endl;

//

// Попытка недопустимого приведения.

//

Bl &rrbl = bl;

cout << "Try invalid cross-casting; object ID:"

<< typeid(rrbl).name() << endl;

B2 &rrb2 = dynamic_ca3t<B2&>(rrbl);

cout << "OK..." << endl;

} catch(bad_cast) {

cout << "Cast failed." << endl;

} catch(bad_typeid) {

cout << "Typeid failed." << end1;

}

return 0;

}

Вывод программы:

Downcasbing from Bl; object. ID: D

OK. . .

Cross-castind from Bl to B2; object ID: D

OK. . .

Try invalid cross-casting; object ID: Bl

Cast failed.

Перекрестное приведение типа, т. е. такое, при котором классы хотя и относятся к одной иерархии, но находятся на разных ее “ветвях”, допускается и для обычных приведений, но в этом случае вряд ли вы получите сколько-нибудь осмысленный результат.

Действие операции dynamic_cast при перекрестном приведении типов можно представить следующим образом. Сначала ищется наивысший класс иерархии, являющий производным сразу от обоих классов, участвующих в преобразовании. Указатель приводится к этому классу (нисходящее преобразование). После этого он возводится до класса результата (преобразование от производного класса к базовому, что можно сделать всегда).

Листинг 13.4. Приведение от виртуального базового класса

//////////////////////////////////////

// VBaseCast.срр: Приведение виртуального базового класса.

//

#include <iostream.h>

#include <typeinfo.h>

#pragma hdrstop

#include <condefs.h>

class VBase { // Виртуальный базовый класс. public:

virtual ~VBase() {} 1;

class Bl: public virtual VBase {};

class B2: public virtual VBase {};

class D: public Bl, public B2 {}; // Производный класс.

//

// Вспомогательная функция, аргументом которой

// может' быть любой класс данной иерархии.

//

void Report(VBase *pvb)

{

try {

cout << " ... Object ID: "

<< typeid (*pvb).name() << endl;

}

catch(bad_typeid) {

cout << " ...'Bad typeid in Report()."<< endl;

}

xin-. ilia in ( ) {

D d;

Bl bl;

{

Base *pvb = &d;

cout << "Original class: " << typeid(*pvb).name();

//не корректное приведение - pvb ссылается на объект //Производного класса.

//

Report (dynamic_cast<D*> (pvb) ) ;

pvb = o.al;

cout<< "Original class: " << typeid(*pvb).name();

//

// Следующее приведение не удается, поскольку объект,

// на который ссылается pvb, не является D. В Report()

// выбрасывается.bad_typeid, т.к. аргумент нулевой.

//

Report(dynamic cast<D*>(pvb));

} catch(bad__typeid) {

cout << " ... Bad typeid in main()." << end1;

}

return 0;

}

Программа выводит:

Original class: D ... Object ID: D

Original class: B1 ... Bad typeid in Report ().

При преобразованиях типа на некоторой иерархии, особенно нисходящих и перекрестных, применяйте операцию dynamic_cast, а не статическое приведение. Динамическое приведение типа обладает неоспоримыми преимуществами и к тому же не вносит в код никаких дополнительных издержек, если приведение можно осуществить на этапе компиляции. Другими словами, в таких случаях, если это достаточно безопасно, dynamic_cast генерирует точно такой же код, что и static_cast.

Заключение

Управление исключениями, идентификация типа времени выполнения и специальные операции приведения составляют в своем роде единство, которое, при грамотном к нему подходе, позволяет значительно повысить надежность и устойчивость создаваемого программного обеспечения. Конечно, ко всему этому придется довольно долго привыкать, если раньше вы программировали, скажем, только на традиционном С.

Этой главой мы заканчиваем описание стандартного языка C++. Следующая часть книги посвящена визуальным средствам C++Builder и связанными с ними особенностям языка этой системы.